Lua

学习文档:LuatOS 文档

在线环境:LuatOS 在线模拟 - lua在线测试

官网:official::The Programming Language Lua

官方API手册:official::Lua 5.3 Reference Manual - contents

中文API手册:Lua 5.3 参考手册 - 目录 (luatos.com)


一、Lua基础

1 下载安装

下载安装:

  1. 下载:official::Lua Binaries Download (sourceforge.net)
  2. 将解压结果放到自定义的一个位置
  3. 编辑环境变量:环境变量——系统变量——Path——新建D:\lua(假设在D盘下)
  4. 在cmd中运行lua,如果不行就看exe后面的数字是什么(版本),假设是lua54.exe,则可以输入lua54即可进入lua环境,可以进入表明环境添加成功。
  5. 可以在cmd中直接lua54 name.lua运行相应的lua程序。

2 数字变量

作用域:lua中声明的变量默认是全局变量,在其他文件中也可以使用。如果只想在当前文件夹中使用需要加上local

空值:lua中没有被声明过的变量都是nil(相当于null值)

数字型:lua中的数字类型是number(不同于c,c有int,float等)。但是因为lua是用c写的,number实际上是c里面的double

-- 这是行注释

--[[
这是块注释,
块注释可以
注释多行!
]]

-- 声明全局变量(直接赋值就是声明,类似python)
a = 1
b = 2

-- 声明局部变量
local a = 1

-- c没被声明过,打印是nil
print(c)

-- 多个变量赋值(a=1,b=2)
a,b = 1,2
-- 左边变量多余右边时,多出的变量会赋值为nil(c=nil)
a,b,c = 1,2

-- 其他数字
a = 0x11 -- 十六进制
b = 2e10 -- 2 * 10的10次方

3 运算符

-- 数字型
a = b + c
a = b - c
a = b * c
a = b / c
a = b // c -- 整除
a = b ^ c -- a = b的c次方
a = b << c -- a = b左移c位
a = b >> c

4 字符串

字符串下标从1开始,可以填入负数进行反向遍历(-1是最后一个字符,依次向前,类似python)

-- 声明字符串
a = "abcdef"

-- 声明带转义字符的字符串(打印时转义字符会被转义)
b = "abc\ndef"

-- 声明多行字符(字符和文字结构直接全部保留,怎么输入就怎么输出(相当于html的textarea))
c = [[abc
\ndef
gg]]

-- 连接字符串
c = a..b

-- 数字转字符串
char = tostring(10)
-- 字符串转数字(转化失败则输出nil)
num = tonumber("10")

-- 获取字符串长度(字符串前面加个井号,像上述的b长度则为7,因为\n算一个)
len = #a
-- 一些API

-- ASCII码构造字符串(参数:字符值)
s = string.char(0x30, 0x31, 0x32, 0x33)

-- 找出字符串中指定区间的ASCII值(参数:字符串;起始索引,可不填默认1;结束索引,可不填默认1)
c = string.byte(str, [start], [end])

-- 0x00不会让字符串结束(不同于c)
s = string.char(0x30, 0x00, 0x31, 0x32, 0x33)
print(s)
print(#s)


-- 语法糖,直接使用:,不需要写string
local s = "123456789"
print(s:byte(2)) -- 调用了string.byte()函数


-- 格式化输出,和c类似
string.format("%d, %d", 1, 2)

-- 获得字符串长度
string.len()
s:len()
#s

-- 将字符串转为小写后返回转化后的字符串,原字符串不变
s:lower()

5 函数

函数返回值默认是nil

函数传递变量时按顺序赋值,如果后面的变量没被赋值(函数需要传递三个变量但是实际上只传递了两个),那么后续的变量使用时值就为nil

函数返回值存在多个时,可以对返回值进行解包。

-- 声明函数(方法一)
function function_name(...)
-- body
end

-- 声明函数(方法二)
function_name = function(...)
-- body
end
-- 案例
f = function(a,b,c)
return a,b,c
end

-- 输出1 2 nil
print(f(1,2))
local i,j = f(1,2)
-- 输出1
print(i)
-- 输出2
print(j)

6 数组table

类似于python的list

table的下标是从1开始而不是0

table遇到nil会停下。所以如果直接给table中的值赋值给nil时,table会认为到nil的位置就是table的结尾了,哪怕后面还有值。

-- 声明table
a = {1, "ac", {}, function() end}

-- 输出1
print(a[1])

-- 获取table长度(和字符串一样前面加一个井号)
len = #a

-- 此时a[0]和a[5]后面的元素都nil,可以直接给a[5]赋值,这样table长度就会发生变化
a[5] = "gg"
-- 一些API

-- 在尾部插入数据(参数:table名,插入值)
table.insert(table, value)

-- 在指定位置插入(参数:table名,下标,插入值)
table.insert(table, index, value)

-- 移除元素同时返回被移除的元素(参数:table名,下标)
local s = table.remove(table, index)
-- 补充说明

-- 定义a
a = {1, "ac", {}, function() end}

-- 用循环遍历table
for key, value in ipairs(a) do
print(key, value)
end

-- 尝试给a[0]赋值,然后使用循环遍历发现不会遍历到a[0],同时table长度不变,但是直接访问a[0]的值是可以的
a[0] = "0"

-- 尝试给a[6]赋值,然后使用循环遍历发现不会遍历到a[6],同时table长度不变,但是直接访问a[6]的值是可以的
a[6] = 6

-- 假设此时已经给a[6]赋值了,再次insert,会发现a[5]被填入了数据,同时table的长度直接变成了6,如果再进行一次insert,a[6]的值不会覆盖,新插入的值会插入到a[7]当中
table.insert(a, "five")
table.insert(a, "seven")

-- 同理假设此时给a[6]赋值,remove一个数据,会发现a[5]之前的数据会向前移动一位,但是a[6]不会
table.remove(a, 3)

7 字典table

table可以作为字典使用。

-- 声明table
a = {
a = 1,
b = "123456",
c = function()

end,
d = 123456,
[",;"] = 123456
}

-- 正常下标可以索引查找值也可以点查找值
print(a["a"])
print(a.a)

-- 异常下标只可以索引查找
print(a[",;"])

-- 对不存在的下标可以直接赋值
a["abc"] = "abcdefg"
print(a.abc)

-- 打印不存在的下标,输出的值为nil
print(a.null)

8 全局表

在lua中有一个特殊的table是_G_G存储所有的全局变量

-- 查看_G table
print(_G)

-- 定义全局变量后查看
a = 1
print(_G.a)

-- 打印所有全局变量
for key, value in pairs(_G) do
print(key, value)
end

-- table也是全局变量存在_G中,同时table里的函数是table的值,所以可以如下打印
print(_G.table)
print(_G.table.insert)

9 条件判断

在lua中,只有nilfalse才代表假,其他都是真,包括0这些。

-- 声明布尔类型
a = true
b = false

-- 值比较
print(1 > 2)
print(1 < 2)
print(1 == 2)
print(1 >= 2)
print(1 ~= 2) -- 这是不等于(不是!=,和其他语言不通)

-- 与或非
print(a and b) -- a为假返回a,a真b假返回b,a真b真返回b
print(a or b) -- 返回第一个为真的变量值,都为假返回最后一个
print(not a) -- 返回true或者false

-- 短路求值
print(a > 10 and "yes" or "no")

-- if判断
if 1 > 10 then
print("1 > 10")
elseif 1 < 10 then
print("1 < 10")
else
print("no")
end

10 循环

注:lua中没有n--n-=1这种操作,只能是n = n - 1

-- for循环,for后面的是初值,结束值,步长(不写默认是1,可以是负数)
for i = 1, 10, 2 do
print(i)
if i == 5 then break end
end

-- 在for中不可以修改i,修改i相当于新建了一个变量
for i = 10, 1, -1 do
print("first:"..tostring(i)) -- 按顺序10到1
i = 5 -- 相当于 local i = 5
print("local:"..tostring(i)) -- 重新新建了一个i,i一直是5
end

-- while循环
local n = 10
while n > 1 do
n = n - 1
print(n)
end

-- for循环整个table
local t = {"a", "b", "c", "d"}
for i=1, #t do
print(i, t[i])
end

二、Lua进阶

1 引入文件

利用require可以引入并执行其他lua文件, 注意:

  1. 引入时会执行文件
  2. 引入不需要带扩展名(因为package.path有一条是.\?.lua,所以会自动匹配lua尾缀的文件)
  3. 目录层级分割符号用"."而不是"/"
  4. 多次引入也只会执行一次
  5. 引入时会从package.path中的路径里进行匹配查找,如果将某一个路径直接加入到package.path后,则后续引入时就不需要再加前缀文件夹名了(同理可以引入父文件夹的lua文件)
  6. 引入的文件如果有返回值则返回返回值,没有的话则返回true
-- /path/hello2.lua
print("hello world two")
-- hello.lua
_G.count = _G.count + 1
print("hello world")

-- 模块可以有返回值,若没有默认返回true
return "done num:".._G.count
-- test.lua(未修改package.path)
_G.count = 1

-- 引入子文件夹下路径时,分隔符号用点
require("path.hello2")

-- lua文件可以返回值,同时require时,子lua程序会执行
local r = require("hello")
print(r)
-- 再次引入发现lua程序不执行,同时_G.count还是2,返回值一样可以接收
local c = require("hello")
print("r:", r)
print("c:", c)

-- _G.count还是2
print("count:", _G.count)


--[[
输出结果如下:
hello world two
hello world
r: done num:2
r: done num:2
c: done num:2
count: 2
]]
-- test.lua(修改package.path,直接引入path下文件)
-- 查看路径
print("\n"..package.path.."\n")

-- 修改路径
package.path = package.path..";./path/?.lua"

-- 直接引入path下文件
require("Hello2")

-- 发现路径被修改
print("\n"..package.path.."\n")

2 自制模块

平常自制模块时,可以先写一个table,然后向table中加入字段和方法,最后返回这个table。(相当于类封装)

在lua中require的模块默认只会执行一次,所以可以在自制模块中写table,然后返回table,这样可以实现多次执行某个模块中的内容。

-- hello.lua
local hello = {}

function hello.say()
print("hello world")
end

return hello
-- test.lua
local hello = require("hello")

-- 可以多次调用
hello.say()
hello.say()

3 迭代器

lua中内置迭代器,通过迭代器可以依次访问数组中的内容并输出

ipairs迭代器在遍历时,如果遍历到nil,则会停下,哪怕table的nil的后面的元素被赋值了

pairs的内部实际上是调用next()函数,通过next()函数来访问下一个下标的键和值

案例:查看第一章第6节数组table

-- ipairs迭代器(只能遍历数字)
local t = {"a", "b", "c", "d"}
for key, value in ipairs(t) do
print(key, value)
end

-- pairs迭代器(遍历全部内容)
local t = {
apple = "a",
banana = "b",
pear = "c",
orange = "d"
}
for key, value in pairs(t) do
print(key, value)
end

-- next()
local t = {"a", "b", "c", "d"}
print(next(t)) -- 返回1 a
print(next(t, 2)) -- 指定键,返回值对应的下一个键值,返回3 c

-- 快速判断table是否为空
local t = {'nil'}
print(next(t)) -- 返回nil即为空

4 元表和元方法

元表和元方法:official::Lua 5.3 参考手册 (luatos.com)

Lua 中的每个值都可以有一个元表,然后元表中的一些由__开头的特殊的方法就是元方法(类似于python的魔术方法),通过重写元方法可以实现一些不同的操作。

一些元方法:

  • __add:重写加法操作,实现一些不同的加法,比如利用一个表加一个数字。
  • __index:重写索引,当索引不到值的时候,就会执行该元方法。
  • __newindex:新增索引时触发,当重写了该元方法后,新增的索引不会直接赋值到表中,需要手动rawset才会新增到表中
-- 重写__add和__index方法

-- 普通表
local t = {a = 1, b = 2, c = 3}

-- 重写了元方法的元表
local mt = {
-- 重写加法
__add = function(t1, t2)
if type(t1) == "table" and type(t2) == "number" then
return t1.a + t2
elseif type(t1) == "number" and type(t2) == "table" then
return t1 + t2.a
else
return "add error"
end
end,
-- 保底索引(当表中索引不到时会索引元表中的索引)
-- __index = {
-- abc = "abc",
-- def = "def"
-- },
-- 重写索引(函数重写)
__index = function(table, key)
return key.." is not exist"
end
}

-- 设置mt的元方法覆盖t的元方法
setmetatable(t, mt)

-- 成功实现一个表加一个数字
print(t + 10)
print(5 + t)
print(t + t)

-- 成功索引了一个不存在的下标
print(t.d)
print(t.abc)
-- 重写__newindex
-- 普通表
local t = {a = 1, b = 2, c = 3}

-- 重写了元方法的元表
local mt = {
__newindex = function(t, k, v)
print("set: " .. k .. " = " .. v)
-- 不执行此函数值不会被设置,此函数不会触发任何元方法
-- rawset(t, k, v)
-- 不可以直接设置元表,否则会死循环(因为赋值就会调用__newindex)
-- t[k] = v
end,
}
setmetatable(t, mt)
t.d = 10
print(t.d) -- 还是nil,需要rawset后才能设置

5 面向对象

table也有类似string的语法糖。当函数的第一个参数是table时,那么t.func(t, num)可以简写成t:func(num)

面向对象:

  • 封装(Person为例):实际上是为Person写一个new函数,在new函数中,再创建一个self表,然后给self表赋值,赋值完了以后修改这个表的__indexPerson,最后返回self。这样,实际上返回使用的是self表。当self表中不存在内容时,因为修改了__indexPerson,所以就会去找Person。所以实际使用上感觉是在使用Person类。
-- table语法糖
local t = {
a = 0,
add = function(tab, sum)
tab.a = tab.a + sum
end
}
-- 相当于t.add(t, 10)
t:add(10)
print(t.a)
-- 封装


-- person.lua
local Person = {}

-- self的元表
local SelfMt = {
-- 设置 __index 为 Person,以便 self 可以访问 Person 中的方法,同时不存在的方法不能访问
__index = function (t, k)
if Person[k] then
return Person[k]
else
error("attempt to access a nil value or nil function")
end
end,
-- 重写__newindex,当self中不存在该键时,不能赋值
__newindex = function(t, k, v)
if Person[k] then
rawset(t, k, v)
else
error("attempt to modify a read-only table")
end
end
-- 其他需要扩展的可以自行扩展
-- ...
}

function Person:new(name, age)
local self = {}
self.name = name
self.age = age

setmetatable(self, SelfMt)
return self
end

function Person:sayHello()
print("Hello, my name is " .. self.name .. " and I am " .. self.age .. " years old.")
end

return Person


-- test.lua
local Person = require("person")

local person = Person:new("John", 30)
person:sayHello()

person.name = "Jane"
person.age = 25

person:sayHello()

-- 异常:attempt to modify a read-only table
-- person.bag = "bag"
-- 异常:attempt to access a nil value or nil function
-- person:goodbye()
-- 继承


-- person.lua 其他和 封装的 person.lua 一样,新加一个函数
function Person:sayName()
print("My name is " .. self.name)
end


-- student.lua
local Person = require("person") -- 引入 Person 模块

local Student = {}

-- 重新设置元表(先找子类,再找父类)
local SelfMt = {
__index = function (t, k)
if Student[k] then
return Student[k]
elseif Person[k] then
return Person[k]
else
error("attempt to access a nil value or nil function")
end
end,
__newindex = function(t, k, v)
if Student[k] then
rawset(t, k, v)
elseif Person[k] then
rawset(t, k, v)
else
error("attempt to modify a read-only table")
end
end
-- 其他需要扩展的可以自行扩展
-- ...
}

function Student:new(name, age, grade)
-- 调用父类构造函数
local self = Person:new(name, age)
-- 清空父类元表
setmetatable(self, nil)
-- 添加额外的属性
self.grade = grade
-- 设置新的子类元表
setmetatable(self, SelfMt)

return self
end

function Student:sayHello()
-- 重写父类的方法
print("Hello, my name is " .. self.name .. ", I am " .. self.age .. " years old, and I'm in grade " .. self.grade .. ".")
end

return Student


-- test.lua
local Student = require("student")

local student = Student:new("Alice", 16, 10)
-- sayHello() 被 Student 重写
student:sayHello()
-- sayName() 继承自 Person
student:sayName()

6 继承框架

在普通进行类继承的时候,需要一直修改元表。因此此处我们可以写一个元表管理器,每次调用这个元表管理器构建元表。

框架若实现,该框架也可以实现多态。子对象可以使用和修改父对象的相关内容。

-- inheritance.lua(目前结果失败)
-- 封装问题:索引会递归,__index调试t = parents原因不明
-- 继承问题:MetaManager:inherit 存在问题,会找不到self.parents

local M = {}

-- 创建一个元表管理器
local MetaManager = {}

-- 初始化元表管理器
function MetaManager:init()
local self = {}
setmetatable(self, { __index = self })
self.parents = {}
self.canSearchNil = true
self.canModifyNil = true
end

-- 继承父类
function MetaManager:inherit(parent)
table.insert(self.parents, parent)
end

-- 重写 __index 查找方法
function MetaManager:index(t, k)
print(t, k)
for _, parent in ipairs(self.parents) do
if parent[k] then
return parent[k]
end
end

if self.canSearchNil then
return nil
else
error("attempt to access a nil value or nil function")
end
end

-- 重写 __newindex 赋值方法
function MetaManager:newindex(t, k, v)
if self.canModifyNil then
rawset(t, k, v)
return
else
for _, parent in ipairs(self.parents) do
if parent[k] then
rawset(t, k, v)
return
end
end
error("attempt to modify a read-only table")
end
end

-- 创建一个新对象
function M:create(obj)
local new_obj = obj
new_obj.__metaManager = MetaManager
new_obj.__metaManager:init()
setmetatable(obj, {__index = new_obj.__metaManager.index, __newindex = new_obj.__metaManager.newindex})
return new_obj
end

-- 进行继承
function M:inherit(obj, parent)
local new_obj = obj
new_obj.__metaManager = parent.__metaManager
new_obj.__metaManager:inherit(parent)
return new_obj
end

-- 设置是否可以访问不存在的属性
function M:setCanSearchNil(obj, canSearch)
if obj.__metaManager then
obj.__metaManager.canSearchNil = canSearch
end
end

-- 设置是否可以修改不存在的属性
function M:setCanModifyNil(obj, canModify)
if obj.__metaManager then
obj.__metaManager.canModifyNil = canModify
end
end

return M
-- person.lua(预计效果)
local Inheritance = require("inheritance")

local Person = {}

function Person:new(name, age)
local self = Inheritance:create()
self.name = name
self.age = age
Inheritance:setCanModifyNil(self, false)
return self
end

function Person:sayHello()
print("Hello, my name is " .. self.name .. " and I am " .. self.age .. " years old.")
end

function Person:sayName()
print("My name is " .. self.name .. ".")
end

return Person

-- student.lua(预计效果)
local Inheritance = require("inheritance")
local Person = require("person")

local Student = {}

function Student:new(name, age, grade)
local self = Inheritance:create(Student)
self = Inheritance:inherit(self, Person)
Person:new(self, name, age)
self.grade = grade
Inheritance:setCanModifyNil(self, false)
return self
end

function Student:sayHello()
print("Hello, my name is " .. self.name .. ", I am " .. self.age .. " years old, and I'm in grade " .. self.grade .. ".")
end

return Student
-- test.lua(预计效果)
local Person = require("person")
-- local Student = require("student")

local person = Person:new("John", 20)
person:sayHello()
person:sayName()


-- local student = Student:new("Alice", 16, 10)
-- -- sayHello() 被 Student 重写
-- student:sayHello()
-- -- sayName() 继承自 Person
-- student:sayName()

7 协程

一个lua虚拟机是一个单线程。

创建协程后返回时是thread,可以通过协程函数来操作thread

-- 创建一个协程(参数function,返回值thread),创建后不会立刻执行
local co = coroutine.create(function()
print("Hello World!!")
end)

-- 开始或继续协程
coroutine.resume(co)


-- 其他协程代码
local co = coroutine.create(function()
print("Hello World!!")
-- 此处会暂停协程,直到下次再resume(参数:返回值,接收值:resume传递的额外参数)
local r1, r2, r3 = coroutine.yield(1,2,3)
print("continue run~~~",r1, r2, r3)
end)

-- 返回true 1 2 3(true是协程函数返回的,1 2 3是自己附加的)
print("yield:", coroutine.resume(co))
-- 继续协程,传递值给yield接收,打印continue run~~~ 4 5 6
coroutine.resume(co, 4, 5, 6)

-- 协程结束后状态是dead
print(coroutine.status(co))
-- 无法继续,返回false
print(coroutine.resume(co))

8 二进制与结构体


三、xLua

1 基础概念

源码:official::Tencent/xLua

普通更新:普通更新是指应用程序需要停止运行,用户手动从应用商店下载并安装新版本的过程,期间应用不可用。

热更新:热更新是在应用程序运行时,通过下载并应用增量更新,实现实时修复或新增功能,无需重启应用,用户使用不受影响。

xLua热补丁技术支持在运行时把一个C#实现(函数,操作符,属性,事件,或者整个类)替换成Lua实现,意味着你可以∶

  1. 平时用C#开发
  2. 运行也是C#,性能秒杀Lua
  3. 有bug的地方利用Lua脚本fix了,下次整体更新时可以把Lua的实现换回正确的C#实现,更新时甚至可以做到不重启游戏
  4. 这个新特性iOS,Android , Window,Mac都测试通过了,目前在做一些易用性优化

特点:

  • 简洁易用,容易上手
  • 可扩展性高,添加自定义的CS模块或者第三方插件非常方便
  • 大厂维护,可靠
  • 特色:HotFix,关于这个HotFix是其他热更lua框架所不具备的,也是他最大的优势和特色之一,原理就是通过特性标记然后在IL逻辑层判断修改逻辑,使程序支持热更的lua逻辑代码而不是走之前的C#逻辑。

热更新流程:手游的热更新流程很简单,只是启动时检测下是否有新版本文件,有的话就下载覆盖老文件,然后启动。

使用原因:下载的文件如果是图片,模型这些是没问题的,但如果是Unity原生的代码逻辑,无论是以前的Mono AOT或者后来的il2cpp,都是编译成native code , iOS下是跑不了的。解决办法就一个,别用native code和jit,解析执行就可以了。包括xLua在内的所有热更新支持方案都是通过“解析执行”来实现代码逻辑热更新。

安装流程:

  1. official::Releases · Tencent/xLua (github.com)中下载Source code
  2. 找到Assets,将里面的所有文件拖入Assets中。
  3. 如果报错找不到MemberInfo,加上代码using System.Reflection;即可。

2 简单使用

引入XLua,现在Start里创建LuaEnv,然后执行代码。最后记得释放LuaEnv

// 一次性使用(不引入XLua)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestLua : MonoBehaviour
{
private void Start()
{
XLua.LuaEnv luaEnv = new XLua.LuaEnv();
luaEnv.DoString("print('Hello World!')");
luaEnv.Dispose();
}
}
// 持续使用,更节省资源
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;

public class TestLua : MonoBehaviour
{
// luaEnv写成字段
private LuaEnv luaEnv;

// Start is called before the first frame update
private void Start()
{
// 创建LuaEnv
luaEnv = new LuaEnv();
// 利用Lua执行cs代码
luaEnv.DoString("CS.UnityEngine.Debug.Log('Hello Lua')");
// 执行Lua代码
luaEnv.DoString("print('Hello World')");
}

private void OnDestroy()
{
// 释放LuaEnv
luaEnv.Dispose();
}
}

3 载入Lua

可以通过载入的方式执行Lua代码,假设文件叫HelloWorld.lua.txt

此处使用Resources载入方式。Resources文件夹需要在Assets中存在,可以存在多个,位置可以任意,程序会依次寻找所有Resources中的文件直到找到第一个匹配的。

注:在使用luaEnv.DoString("require 'HelloWorld'");的时候,文件保存的编码不能是UTF-8 With BOM,不然会报错unexpected symbol near '<\239>' stack traceback,修改成UTF-8 Without BOM即可。

-- HelloWorld.lua.txt
print('Hello World')
// 载入Lua
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;

public class TestLua : MonoBehaviour
{
private LuaEnv luaEnv;

private void Start()
{
luaEnv = new LuaEnv();
// 不用写.txt,因为是泛型TextAsset加载,知道是加载txt文件
TextAsset txt = Resources.Load<TextAsset>("HelloWorld.lua");
// 打印,不会执行lua代码
print(txt);
// 将txt内容转成字符串后执行lua代码
luaEnv.DoString(txt.ToString());
// 直接包含HelloWorld.lua.txt文件内容并执行
luaEnv.DoString("require 'HelloWorld'");
}

private void OnDestroy()
{
luaEnv.Dispose();
}
}

4 自定义Loader

在xLua中可以自定义Loader,当luaEnv.AddLoader(MyLoader);(MyLoader参数和返回值不能变,因为AddLoader参数值是委托)后且执行了require 'HelloWorld'的时候,包含进来的文件会按照自定义的规则,返回内容并执行,不会直接执行文件中的内容。

在Loader中可以自定义载入路径。

载入txt时中文乱码,需要将编码改成UTF-8

Windows 10更新1903版本后,记事本的几种编码模式改了名称:

  • Unicode => UTF-16 LE
  • Unicode big endian => UTF-16 BE
  • 旧版的“UTF-8”相当于新版的“带有BOM的UTF-8”
  • 新版的“UTF-8”实质上是“不带BOM的UTF-8”
// 载入Lua
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;

public class TestLua : MonoBehaviour
{
private LuaEnv luaEnv;
private void Start()
{
luaEnv = new LuaEnv();
// 自定义Loader(要在require之前)
luaEnv.AddLoader(MyLoader);
// 执行require
luaEnv.DoString("require 'HelloWorld'");
}

private byte[] MyLoader(ref string file)
{
if (file == "HelloWorld")
{
string s = "print('Load Hello World!')";
// 返回字节并执行
return System.Text.Encoding.UTF8.GetBytes(s);
}

string res = "print('Load Error!')";
// 返回字节并执行
return System.Text.Encoding.UTF8.GetBytes(res);
}

private void OnDestroy()
{
luaEnv.Dispose();
}
}
// 自定义路径载入
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XLua;

public class TestLua : MonoBehaviour
{
private LuaEnv luaEnv;
private void Start()
{
luaEnv = new LuaEnv();
luaEnv.AddLoader(MyLoader);
luaEnv.DoString("require 'HelloWorld'");
}

private byte[] MyLoader(ref string file)
{
// 自定义路径(文件需要在Assets/StreamingAssets下)
string path = Application.streamingAssetsPath + "/" + "extra" + ".lua.txt";
// 读出字节并执行后返回
return File.ReadAllBytes(path);
}

private void OnDestroy()
{
luaEnv.Dispose();
}
}

5 变量使用

局部变量不能直接使用,需要通过全局变量函数进行访问。

-- HelloWorld.lua.txt
a = 1
str = "myString"
isRun = false

local b = 10
function addB(one, two)
return one + two + b
end

person = {
name = "john",
age = 123
}
// 载入并使用变量
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XLua;

public class TestLua : MonoBehaviour
{
private LuaEnv luaEnv;
private void Start()
{
luaEnv = new LuaEnv();
luaEnv.DoString("require 'HelloWorld'");
// 载入全局变量
int a = luaEnv.Global.Get<int>("a");
string str = luaEnv.Global.Get<string>("str");
bool isRun = luaEnv.Global.Get<bool>("isRun");

Debug.Log(a);
Debug.Log(str);
Debug.Log(isRun);

// 获取Lua函数
var getBFunc = luaEnv.Global.Get<LuaFunction>("addB");
// 调用Lua函数并获取返回值,可以由此获得局部变量值(传递参数和参数类型唤醒函数)
var result = getBFunc.Call(new object[] { 1, 2 }, new Type[] { typeof(int), typeof(int) });
int sums = (int)result[0];
// 打印结果
Debug.Log(sums); // 应该输出: 10

// 获得全局表格
Person person = luaEnv.Global.Get<Person>("person");
Debug.Log(person.name + person.age.ToString());
}


private void OnDestroy()
{
luaEnv.Dispose();
}
}

class Person
{
public string name;
public int age;
}

6 接口映射

-- HelloWorld.lua.txt
person =
{
name = "abc",
age = 20,
myPrint = function(self, a, b)
print(a + b)

CS.UnityEngine.Debug.Log("你好,世界")
return a + b
end
}